<?php

declare(strict_types=1);

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Tests\Fixer\FunctionNotation;

use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
use PhpCsFixer\Fixer\FunctionNotation\NativeFunctionInvocationFixer;
use PhpCsFixer\Tests\Test\AbstractFixerTestCase;

/**
 * @internal
 *
 * @covers \PhpCsFixer\Fixer\FunctionNotation\NativeFunctionInvocationFixer
 *
 * @extends AbstractFixerTestCase<\PhpCsFixer\Fixer\FunctionNotation\NativeFunctionInvocationFixer>
 *
 * @author Andreas Möller <am@localheinz.com>
 *
 * @phpstan-import-type _AutogeneratedInputConfiguration from \PhpCsFixer\Fixer\FunctionNotation\NativeFunctionInvocationFixer
 *
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
 */
final class NativeFunctionInvocationFixerTest extends AbstractFixerTestCase
{
    /**
     * @dataProvider provideInvalidConfigurationCases
     *
     * @param array<string, mixed> $configuration
     */
    public function testInvalidConfiguration(array $configuration, string $expectedExceptionMessage): void
    {
        $this->expectException(InvalidFixerConfigurationException::class);
        $this->expectExceptionMessage($expectedExceptionMessage);

        $this->fixer->configure($configuration);
    }

    /**
     * @return iterable<string, array{array<string, mixed>, string}>
     */
    public static function provideInvalidConfigurationCases(): iterable
    {
        yield 'unknown key' => [
            ['foo' => 'bar'],
            '[native_function_invocation] Invalid configuration: The option "foo" does not exist.',
        ];

        yield 'null' => [
            ['exclude' => [null]],
            '[native_function_invocation] Invalid configuration: The option "exclude" with value array is expected to be of type "string[]", but one of the elements is of type "null".',
        ];

        yield 'false' => [
            ['exclude' => [false]],
            '[native_function_invocation] Invalid configuration: The option "exclude" with value array is expected to be of type "string[]", but one of the elements is of type "bool".',
        ];

        yield 'true' => [
            ['exclude' => [true]],
            '[native_function_invocation] Invalid configuration: The option "exclude" with value array is expected to be of type "string[]", but one of the elements is of type "bool".',
        ];

        yield 'int' => [
            ['exclude' => [1]],
            '[native_function_invocation] Invalid configuration: The option "exclude" with value array is expected to be of type "string[]", but one of the elements is of type "int".',
        ];

        yield 'array' => [
            ['exclude' => [[]]],
            '[native_function_invocation] Invalid configuration: The option "exclude" with value array is expected to be of type "string[]", but one of the elements is of type "array".',
        ];

        yield 'float' => [
            ['exclude' => [0.1]],
            '[native_function_invocation] Invalid configuration: The option "exclude" with value array is expected to be of type "string[]", but one of the elements is of type "float".',
        ];

        yield 'object' => [
            ['exclude' => [new \stdClass()]],
            '[native_function_invocation] Invalid configuration: The option "exclude" with value array is expected to be of type "string[]", but one of the elements is of type "stdClass".',
        ];

        yield 'not-trimmed' => [
            ['exclude' => ['  is_string  ']],
            '[native_function_invocation] Invalid configuration: Each element must be a non-empty, trimmed string, got "string" instead.',
        ];

        yield 'unknown set' => [
            ['include' => ['@xxx']],
            '[native_function_invocation] Invalid configuration: Unknown set "@xxx", known sets are "@all", "@internal" and "@compiler_optimized".',
        ];

        yield 'non-trimmed set' => [
            ['include' => [' x ']],
            '[native_function_invocation] Invalid configuration: Each element must be a non-empty, trimmed string, got "string" instead.',
        ];
    }

    /**
     * @param _AutogeneratedInputConfiguration $configuration
     *
     * @dataProvider provideFixCases
     */
    public function testFix(string $expected, ?string $input = null, array $configuration = []): void
    {
        $this->fixer->configure($configuration);
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<array{0: string, 1?: null|string, 2?: _AutogeneratedInputConfiguration}>
     */
    public static function provideFixCases(): iterable
    {
        yield [
            '<?php

\is_string($foo);
',
        ];

        yield [
            '<?php

\is_string($foo);
',
            '<?php

is_string($foo);
',
        ];

        yield [
            '<?php

class Foo
{
    public function bar($foo)
    {
        return \is_string($foo);
    }
}
',
        ];

        yield [
            '<?php

json_encode($foo);
\strlen($foo);
',
            '<?php

json_encode($foo);
strlen($foo);
',
        ];

        yield [
            '<?php

class Foo
{
    public function bar($foo)
    {
        return \IS_STRING($foo);
    }
}
',
            '<?php

class Foo
{
    public function bar($foo)
    {
        return IS_STRING($foo);
    }
}
',
        ];

        yield 'fix multiple calls in single code' => [
            '<?php

json_encode($foo);
\strlen($foo);
\strlen($foo);
',
            '<?php

json_encode($foo);
strlen($foo);
strlen($foo);
',
        ];

        yield [
            '<?php $name = \get_class($foo, );',
            '<?php $name = get_class($foo, );',
        ];

        yield [
            '<?php

is_string($foo);
',
            null,
            ['exclude' => ['is_string']],
        ];

        yield [
            '<?php

class Foo
{
    public function bar($foo)
    {
        return is_string($foo);
    }
}
',
            null,
            ['exclude' => ['is_string']],
        ];

        yield [
            '<?php echo count([1]);',
            null,
            ['scope' => 'namespaced'],
        ];

        yield [
            '<?php
namespace space1 { ?>
<?php echo \count([2]) ?>
<?php }namespace {echo count([1]);}
',
            '<?php
namespace space1 { ?>
<?php echo count([2]) ?>
<?php }namespace {echo count([1]);}
',
            ['scope' => 'namespaced'],
        ];

        yield [
            '<?php
namespace Bar {
    echo \strLEN("in 1");
}

namespace {
    echo strlen("out 1");
}

namespace {
    echo strlen("out 2");
}

namespace Bar{
    echo \strlen("in 2");
}

namespace {
    echo strlen("out 3");
}
',
            '<?php
namespace Bar {
    echo strLEN("in 1");
}

namespace {
    echo strlen("out 1");
}

namespace {
    echo strlen("out 2");
}

namespace Bar{
    echo strlen("in 2");
}

namespace {
    echo strlen("out 3");
}
',
            ['scope' => 'namespaced'],
        ];

        yield [
            '<?php
namespace space11 ?>

    <?php
echo \strlen(__NAMESPACE__);
namespace space2;
echo \strlen(__NAMESPACE__);
',
            '<?php
namespace space11 ?>

    <?php
echo strlen(__NAMESPACE__);
namespace space2;
echo strlen(__NAMESPACE__);
',
            ['scope' => 'namespaced'],
        ];

        yield [
            '<?php namespace PhpCsFixer\Tests\Fixer\Casing;\count([1]);',
            '<?php namespace PhpCsFixer\Tests\Fixer\Casing;count([1]);',
            ['scope' => 'namespaced'],
        ];

        yield [
            '<?php
namespace Space12;

echo \count([1]);

namespace Space2;

echo \count([1]);
?>
',
            '<?php
namespace Space12;

echo count([1]);

namespace Space2;

echo count([1]);
?>
',
            ['scope' => 'namespaced'],
        ];

        yield [
            '<?php namespace {echo strlen("out 2");}',
            null,
            ['scope' => 'namespaced'],
        ];

        yield [
            '<?php
namespace space13 {
    echo \strlen("in 1");
}

namespace space2 {
    echo \strlen("in 2");
}

namespace { // global
    echo strlen("global 1");
}
',
            '<?php
namespace space13 {
    echo strlen("in 1");
}

namespace space2 {
    echo strlen("in 2");
}

namespace { // global
    echo strlen("global 1");
}
',
            ['scope' => 'namespaced'],
        ];

        yield [
            '<?php
namespace space1 {
    echo \count([1]);
}
namespace {
    echo \count([1]);
}
',
            '<?php
namespace space1 {
    echo count([1]);
}
namespace {
    echo count([1]);
}
',
            ['scope' => 'all'],
        ];

        yield 'include set + 1, exclude 1' => [
            '<?php
                    echo \count([1]);
                    \some_other($a, 3);
                    echo strlen($a);
                    not_me();
                ',
            '<?php
                    echo count([1]);
                    some_other($a, 3);
                    echo strlen($a);
                    not_me();
                ',
            [
                'include' => [NativeFunctionInvocationFixer::SET_INTERNAL, 'some_other'],
                'exclude' => ['strlen'],
            ],
        ];

        yield 'include @all' => [
            '<?php
                    echo \count([1]);
                    \some_other($a, 3);
                    echo \strlen($a);
                    \me_as_well();
                ',
            '<?php
                    echo count([1]);
                    some_other($a, 3);
                    echo strlen($a);
                    me_as_well();
                ',
            [
                'include' => [NativeFunctionInvocationFixer::SET_ALL],
            ],
        ];

        yield 'include @compiler_optimized' => [
            '<?php
                    // do not fix
                    $a = strrev($a);
                    $a .= str_repeat($a, 4);
                    $b = already_prefixed_function();
                    // fix
                    $c = \get_class($d);
                    $e = \intval($f);
                ',
            '<?php
                    // do not fix
                    $a = strrev($a);
                    $a .= str_repeat($a, 4);
                    $b = \already_prefixed_function();
                    // fix
                    $c = get_class($d);
                    $e = intval($f);
                ',
            [
                'include' => [NativeFunctionInvocationFixer::SET_COMPILER_OPTIMIZED],
            ],
        ];

        yield [
            '<?php class Foo {
                        public function & strlen($name) {
                        }
                    }
                ',
        ];

        yield 'scope namespaced and strict enabled' => [
            '<?php
                    $a = not_compiler_optimized_function();
                    $b = intval($c);
                ',
            '<?php
                    $a = \not_compiler_optimized_function();
                    $b = \intval($c);
                ',
            [
                'scope' => 'namespaced',
                'strict' => true,
            ],
        ];

        yield [
            '<?php
                    use function foo\json_decode;
                    json_decode($base);
                ',
            null,
            [
                'include' => [NativeFunctionInvocationFixer::SET_ALL],
            ],
        ];

        yield [
            '<?php return [\set() => 42];',
            '<?php return [set() => 42];',
            [
                'include' => ['@all'],
            ],
        ];
    }

    /**
     * @param _AutogeneratedInputConfiguration $configuration
     *
     * @dataProvider provideFixPre80Cases
     *
     * @requires PHP <8.0
     */
    public function testFixPre80(string $expected, ?string $input = null, array $configuration = []): void
    {
        $this->fixer->configure($configuration);
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<array{string, 1?: string, 2?: _AutogeneratedInputConfiguration}>
     */
    public static function provideFixPre80Cases(): iterable
    {
        yield 'include @compiler_optimized with strict enabled' => [
            '<?php
                        $a = not_compiler_optimized_function();
                        $b =  not_compiler_optimized_function();
                        $c = \intval($d);
                    ',
            '<?php
                        $a = \not_compiler_optimized_function();
                        $b = \ not_compiler_optimized_function();
                        $c = intval($d);
                    ',
            [
                'include' => [NativeFunctionInvocationFixer::SET_COMPILER_OPTIMIZED],
                'strict' => true,
            ],
        ];

        yield [
            '<?php
echo \/**/strlen($a);
echo \ strlen($a);
echo \#
#
strlen($a);
echo \strlen($a);
',
            '<?php
echo \/**/strlen($a);
echo \ strlen($a);
echo \#
#
strlen($a);
echo strlen($a);
',
        ];
    }

    /**
     * @param _AutogeneratedInputConfiguration $config
     *
     * @dataProvider provideFix80Cases
     *
     * @requires PHP 8.0
     */
    public function testFix80(string $expected, ?string $input = null, array $config = []): void
    {
        $this->fixer->configure($config);
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<string, array{0: string, 1?: null|string, 2?: _AutogeneratedInputConfiguration}>
     */
    public static function provideFix80Cases(): iterable
    {
        yield 'attribute and strict' => [
            '<?php
                #[\Attribute(\Attribute::TARGET_CLASS)]
                class Foo {}
            ',
            null,
            ['strict' => true],
        ];

        yield 'null safe operator' => ['<?php $x?->count();'];

        yield 'multiple function-calls-like in attribute' => [
            '<?php
                #[Foo(), Bar(), Baz()]
                class Foo {}
            ',
            null,
            ['include' => ['@all']],
        ];
    }

    /**
     * @param _AutogeneratedInputConfiguration $configuration
     *
     * @dataProvider provideFix84Cases
     *
     * @requires PHP 8.4
     */
    public function testFix84(string $expected, ?string $input = null, array $configuration = []): void
    {
        $this->fixer->configure($configuration);
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<string, array{string, 1?: string, 2?: _AutogeneratedInputConfiguration}>
     */
    public static function provideFix84Cases(): iterable
    {
        yield 'property hooks' => [
            <<<'PHP'
                <?php
                class Foo
                {
                    public string $bar = '' {
                        get => $this->bar;
                        set(string $bar) => \strtolower($bar);
                    }
                    public string $baz = '' {
                        get => $this->baz;
                        set(string $baz) { $this->baz = \strtoupper($baz); }
                    }
                }
                PHP,
            <<<'PHP'
                <?php
                class Foo
                {
                    public string $bar = '' {
                        get => $this->bar;
                        set(string $bar) => strtolower($bar);
                    }
                    public string $baz = '' {
                        get => $this->baz;
                        set(string $baz) { $this->baz = strtoupper($baz); }
                    }
                }
                PHP,
            ['include' => ['@all']],
        ];
    }
}
